/**
 * @file util.jsx
 * @author liaoxuezhi@baidu.com
 */
/* eslint fecs-indent: [0, "space", 2, 2] */
import * as React from 'react';
import {createAction} from 'redux-actions';
import * as assign from 'object-assign';
import * as update from 'react-addons-update';
import * as Ajax from '../util/ajax';
import * as qs from 'qs';
import * as template from 'lodash/template';
import * as forEach from 'lodash/forEach';
import * as isPlainObject from 'lodash/isPlainObject';
import * as objectKeys from 'lodash/keys';
import * as isArray from 'lodash/isArray';
import * as moment from 'moment';
import * as qs from 'qs';
import {join as pathJoin, dirname} from 'path-browserify';
const rSchema = /^(get|post|put|delete|patch):/i;
import {alert, confirm} from '../widgets/alert/alert';
import {
  ubb2html,
  html2ubb
} from '../widgets/editor/froala/ubb';
import {browserHistory} from 'react-router';
import * as isEmpty from 'lodash/isEmpty';
import * as keys from 'lodash/keys';

export {
  alert,
  confirm
};

const ifSecondDomain = true;

export function createScopedAction(scopedKey, type, payloadCreator = null, meta = {}) {
  return createAction(type, payloadCreator, () => ({
    scopedKey,
    meta
  }));
}

export const createScopedDispatchAction = (type, payloadCreator = null) => (scopedKey, data, meta = {}) => {
  const actionCreator = createAction(type, payloadCreator, () => ({
    scopedKey,
    meta
  }));

  return dispatch => {
    dispatch(actionCreator(data));
  };
};

export const createAjaxAction = (preferMethod, startType, endType) => (scopedKey, remote, values, ctx = values || {}) => {
  const start = createScopedAction(scopedKey, startType, null, ctx);
  const end = createScopedAction(scopedKey, endType, null, ctx);

  return dispatch => {
    dispatch(start(values));
    return dispatch(end(fetch(remote, values, preferMethod, ctx)));
  };
};

const paramsSerializer = params => qs.stringify(params, {arrayFormat: 'brackets'});

export const fetch = (remote, values, preferMethod, ctx = {}) => {
  const locals = ctx.locals || ctx;
  const api = buildApi(remote, assign({}, locals, isArray(values) ? {rows: values} : values), preferMethod, isArray(values) ? {rows: values} : values);
  const args = [api.url];
  const config = {};
  let data = assign({}, api.data || {});
  api.appended || isPlainObject(values) && (data = assign({}, values, data));
  if (api.type) {
    config.responseType = api.type;
  }

  if (data && api.dataType === 'form-data' && !~'get|delete'.indexOf(api.method)) {
    data = obj2FormData(data);
  }

  api.headers && (config.headers = api.headers);

  if (~'get|delete'.indexOf(api.method)) {
    config.params = assign({}, api.data, data);
    const idx = api.url.indexOf('?');

    if (~idx) {
      const params = qs.parse(api.url.substring(idx + 1));
      Object.keys(params).forEach(key => delete config.params[key]);
    }

    config.paramsSerializer = paramsSerializer;
  } else {

    // 如果数据是数组, 把附带的值设置到 query 中去.
    if (isArray(values) && !api.data) {
      // let query = {};
      // const idx = api.url.indexOf('?');
      // if (~idx) {
      //   query = qs.parse(api.url.substring(idx + 1));
      //   api.url = api.url.substring(0, idx);
      // }
      // query = assign(query, api.data);
      // api.url = `${api.url}?${qs.stringify(query)}`;
      args.splice(0, 1, api.url);
      data = values;
    }

    args.push(data);
  }
  args.push(config);
  return Ajax[api.method === 'delete' ? 'remove' : api.method].apply(this, args);
};

function obj2FormData(data) {
  const fd = new FormData();
  const setter = (value, key) => {
    let type = typeof value;
    if (Array.isArray(value)) {
      value.forEach((v, k) => setter(v, `${key}[${k}]`));
    } else if (type === 'object' && value !== null) {
      Object.keys(value).forEach(k => setter(value[k], `${key}[${k}]`));
    } else {
      fd.append(key, value);
    }
  };

  if (!data) {
    return fd;
  }


  Object.keys(data).forEach((key, _key) => setter(data[key], key));
  return fd;
}


export function createScopedReducer(reducer) {
  return (state = {}, ...args) => {
    const action = args[0];
    const key = action.meta && action.meta.scopedKey || 'unScoped';

    const oldState = state[key] || state['unScoped'];
    args.unshift(oldState);
    const newState = reducer.apply(this, args);

    if (oldState !== newState) {
      state = assign({}, state, {
        [key]: newState
      });
    }

    return state;
  }
}

export const wrapIn = (path, obj) => {
  if (!path) {
    return obj;
  }

  const newObj = {};
  const paths = path.split('.');
  const last =  paths.pop();
  paths.reduce((prev, key) => prev[key] = {}, newObj)[last] = obj;
  return newObj;
};

export const createRequestReducer = (key = 'fetching', path = null) => (state, action) => update(state, wrapIn(path, {
  $merge: {
    ctx: action.meta && action.meta.meta,
    [key]: true,
    invalid: false
  }
}));

export const createReceiveReducer = (key = 'fetching', path = null) => (state, action) => update(state, wrapIn(path, {
  $merge: {
    message: action.payload.msg || action.payload.message,
    [key]: false,
    invalid: action.error,
    error: action.error ? action.payload : null
  }
}));

export const compose = (...fn) => (store, action) => fn.reduceRight((store, fn) => fn(store, action), store);

export const normalizeLink = (link, location = browserHistory.createLocation(window.location), overrideParams = false) => {
  if (link.match(/^https?:/)) {
    return link;
  }

  const groupPrefix = location.pathname.split('/').slice(0, 3).join('/');
  const parts = link.split('?');

  if (!parts[0]) {
    parts[0] = location.pathname;
  } else if (parts[0][0] === '/') {
    if (!ifSecondDomain) {
      parts[0] = parts[0].indexOf('/group/') !== 0 && groupPrefix.indexOf('/group/') ===0 ? (groupPrefix + parts[0]) : parts[0];
    }
  } else {
    parts[0] = pathJoin(dirname(location.pathname), parts[0]);
  }

  if (overrideParams) {
    parts[1] = qs.stringify(assign({}, location.query, parts[1] ? qs.parse(parts[1]) : {}));
  }

  if (location.query && location.query.embed) {
    parts[1] = parts[1] || '';
    parts[1] += (parts[1] ? '&' : '') + 'embed=true';
  }

  return `${parts[0]}${parts[1] ? `?${parts[1]}` : ''}`;
};

export const buildApi = (api, locals = {}, prefer = 'get', data = locals, useFilter = false) => {
  if (typeof api === 'string') {
    let method = rSchema.test(api) ? RegExp.$1 : '';
    method && (api = api.substring(method.length + 1));

    api = {
      method,
      url: api
    };
  } else {
    api = assign({}, api);
  }

  api.method = api.method || prefer || 'get';
  api.url = api.url || '';
  const raw = api.url;
  let url = api.url;
  const idx = url.indexOf('?');
  let search = '';

  if (~idx) {
    search = url.substring(idx + 1);
    url = url.substring(0, idx);
  }

  api.url = tokenize(url, locals);

  if (search) {
    search = (useFilter ? filter(search, locals) : tokenize(search, locals))
      .replace(/(^|&)(?:([a-z0-9_]+)=)?\$\$(?:$|&)/, (_, prefix, key) => prefix + (key ? JSON.stringify(locals) : qs.stringify(locals)));
    api.url = `${api.url}?${search}`;
  }

  api.url = api.url.replace(/^raw:/, '');

  if (~'get|delete'.indexOf(api.method)) {
    api.appended = !!~raw.indexOf('$');
  }

  if (api.data) {
    api.data = dataMapping(api.data, locals, () => api.appended = true, data);
  }

  if (api.headers) {
    api.headers = dataMapping(api.headers, locals, noop, data);
  }

  return api;
};

export const buildLink = (...args) => {
  let api = buildApi(...args);

  if (api.data) {
    return api.url + (~api.url.indexOf('?') ? '&' : '?') + qs.stringify(api.data);
  }

  return api.url;
};

export const noop = () => {};

const resolveMapping = (value, locals, data = locals) => typeof value === 'string' && /^\$(?:([a-z0-9_.]+)|{([^}]+?)})$/.test(value)
  ? resolveVariableAndFilter(value, locals, data)
  : typeof value === 'string' && ~value.indexOf('$') ? tokenize(value, locals) : value;

export const dataMapping = (mapper, locals, onMapped = noop, data = locals) => {
  let formatted = {};
  objectKeys(mapper).forEach(key => {
    const value = mapper[key];
    let keys;

    if (key === '&' && value === '$$') {
      onMapped();
      assign(formatted, data);
    } else if (value === '$$') {
      onMapped();
      formatted[key] = data;
    } else if (value && value[0] === '$') {
      onMapped();
      // formatted[key] = data[value.substring(1)];
      const v = resolveMapping(value, locals, data);
      formatted[key] = v;

      if (v === '__undefined') {
        delete formatted[key];
      }

    } else if (isPlainObject(value) && (keys = objectKeys(value)) && keys.length === 1 &&
      locals[keys[0].substring(1)] && isArray(locals[keys[0].substring(1)])
    ) {

      // 支持只取数组中的部分值这个需求
      // 如:
      // data: {
      //   items: {
      //     '$rows': {
      //        id: '$id',
      //        forum_id: '$forum_id'
      //      }
      //   }
      // }
      const arr = locals[keys[0].substring(1)];
      const mapping = value[keys[0]];

      onMapped();

      formatted[key] = arr.map(raw => {
        let item = null;

        objectKeys(mapping).forEach(key => {
          const value = mapping[key];

          if (key === '&' && value === '$$') {
            item = isPlainObject(item) ? item : {};
            assign(item, raw);
          } else if (key === '&') {
            isPlainObject(item) ? assign(item, resolveMapping(value, assign({}, locals, raw), raw))
              : (item = resolveMapping(value, assign({}, locals, raw), raw));
          } else {
            item = isPlainObject(item) ? item : {};
            item[key] = resolveMapping(value, assign({}, locals, raw), raw);
          }

          // if (key === '&' && value === '$$') {
          //   isPlainObject(item) ? assign(item, raw) : (item = raw);
          // } else if (value === '$$') {
          //   item[key] = raw;
          // } else if (value[0] === '$') {
          //   // item[key] = raw[value.substring(1)];
          //   item[key] = resolveVariable(value, raw);
          // } else if (typeof value == 'string' && ~value.indexOf('$')) {
          //   formatted[key] = tokenize(value, raw);
          // }  else {
          //   item[key] = value;
          // }
        });

        return item;
      });
    } else if (isPlainObject(value)) {
      formatted[key] = dataMapping(value, locals, onMapped, data);
    } else if (typeof value == 'string' && ~value.indexOf('$')) {
      formatted[key] = resolveMapping(value, locals, data);
    } else {
      formatted[key] = value;

      if (value === '__undefined') {
        delete formatted[key];
      }
    }
  });

  return formatted;
};


export const evalExpression = (expression, data) => {
  /* jshint evil:true */
  try {
    const fn = new Function('data', `try{with(data){return !!(${expression});}}catch(e) {return false;}`);
    return data ? fn(data) : fn;
  } catch (e) {
    return data ? false : () => false;
  }
};

export const evalExpression2 = (expression, data, locals = null) => {
  if (typeof expression !== 'string' || !expression) {
    return null;
  }

  if (expression[0] === '$') {
    return tokenize(expression, data, locals);
  } else {
    locals && (data = assign({}, locals, data));
    try {
      if(/return/.test(expression)) {
        return (new Function('data', `try{with(data){ ${expression};}}catch(e) {return null;}`))(data);
      }else {
        return (new Function('data', `try{with(data){return ${expression};}}catch(e) {return null;}`))(data);
      }
    } catch (e) {
      return null;
    }
  }
};

export const isTpl = str => ~str.indexOf('<%');

export const protectFn = fn => (...args) => {
  try {
    return fn(...args);
  } catch (e) {
    return `<span class="text-danger">${e.message}</span>`
  }
};

export const tpl2html = (...args) => {
  const fn = protectFn(template(args[0], {
    imports: tplImports,
    variable: 'data'
  }));

  return args.length > 1 ? fn((args[2] ? assign({}, args[2], args[1]) : args[1]) || {}) : fn;
};

export function formatDuration(value) {
  const unit = ['秒', '分', '时', '天', '月', '季', '年'];
  const steps = [1, 60, 3600, 86400, 2592000, 7776000, 31104000];
  let len = steps.length;
  const parts = [];

  while (len--) {
    if (steps[len] && value >= steps[len]) {
      parts.push(Math.round(value / steps[len]) + unit[len]);
      value %= steps[len];
    } else if (len === 0 && value) {
      parts.push((value.toFixed ? value.toFixed(2) : '0') + unit[0]);
    }
  }

  return parts.join('');
}

export const filters = {
  html: input => escapeHtml(input),
  json: (input, tabSize = 2) => tabSize ? JSON.stringify(input, null, parseInt(tabSize, 10)) : JSON.stringify(input),
  toJson: (input) => {
    let ret;
    try {
      ret = JSON.parse(input);
    } catch(e) {
      ret = null;
    }
    return ret;
  },
  raw: input => input,
  date: (input, format = 'LLL', inputFormat = 'X') => moment(input, inputFormat).format(format),
  number: input => String(input).replace(/\B(?=(\d{3})+(?!\d))/g, ","),
  trim: input => input.trim(),
  percent: (input, decimals = 0) => {
    input = parseFloat(input) || 0;
    decimals = parseInt(decimals, 10) || 0;

    let whole = input * 100;
    let multiplier = Math.pow(10, decimals);

    return parseFloat(Math.floor(whole * multiplier) / multiplier).toFixed(decimals) + '%';
  },
  duration: input => input ? formatDuration(input) : input,
  round: (input, decimals = 0) => {
    if(isNaN(input)) {
      return 0;
    }

    decimals = parseInt(decimals, 10) || 2;

    let multiplier = Math.pow(10, decimals);
    return parseFloat(Math.round(input * multiplier) / multiplier).toFixed(decimals);
  },
  truncate: (input, length, end) => {
    end = end || '...';

    if(length == null) {
      return input;
    }

    length = parseInt(length, 10) || 200;

    return input.substring(0, length) + ((input.length > length) ? end : '');
  },
  url_encode: input => encodeURIComponent(input),
  url_decode: input => decodeURIComponent(input),
  'default': (input, ...rest) => input || rest.join(':'),
  join: (input, glue) => input && input.join ? input.join(glue) : input,
  ubb2html: input => input && !/<\/?\w+\s*[^>]*>/.test(input) ? ubb2html(input) : input,
  html2ubb: input => input && /<\/?\w+\s*[^>]*>/.test(input) ? html2ubb(input) : input,
  split: (input, delimiter=',') => typeof input === 'string' ? input.split(delimiter) : input,
  first: input => input && input[0],
  nth: (input, nth = 0) => input && input[nth],
  last: input => input && (input.length ? input[input.length -1] : null),
  minus: (input, step = 1) => (parseInt(input, 10) || 0) - parseInt(step, 10),
  plus: (input, step = 1) => (parseInt(input, 10) || 0) + parseInt(step, 10),
  pick: (input, path = '&') => Array.isArray(input) ? input.map(item => resolveVariable(path, item)) : resolveVariable(path, input),
  pick_if_exist: (input, path = '&') => Array.isArray(input) ? input.map(item => resolveVariable(path, item) || item) : (resolveVariable(path, input) || input),
  str2date: (input, inputFormat='X', outputFormat = 'X') => input ? filterDate(input, this, inputFormat).format(outputFormat) : '',
  asArray: input => input ? [input] : input,
  n2br: input => input ? String(input).replace(/[\n|\r\n|\r]/g, '<br />') : input
};

export const tokenize = (...args) => {
  if (args.length === 1) {
    return tokenize.bind(null, args[0]);
  }

  let [str, data, locals] = args;

  locals && (data = assign({}, locals, data));
  return str.replace(/(\\)?\$(?:([a-z0-9_\.]+|&)|{([^}]+?)})/ig, _ => resolveVariableAndFilter(_, data));
};

export const filterObj = (obj, data) => {
  if (!obj || typeof obj !== 'object') {
    return obj;
  }

  const newObj = {};

  Object.keys(obj).forEach(key => {
    let value = obj[key];

    if (typeof value === 'string') {
      value = filter(value, data);
    } else if (typeof value === 'object') {
      value = filterObj(value, data);
    }

    newObj[key] = value;
  });

  return newObj;
};

let undef;
export const resolveVariable = (path, locals = {}, data = locals) => {
  if (!path) {
    return undef;
  }

  if (path === '$$') {
    return locals;
  } else if (path[0] === '$') {
    path = path.substring(1);
  }

  if (!path) {
    return undef;
  } else if (path === '&') {
    return data;
  }

  let parts = path.replace(/^{|}$/g, '').split('.');
  return parts.reduce((data, path) => {
    if (path && path[0] === '$' && path[1] === '$') {
      path = resolveVariable(path.substring(2), locals, data);
    }

    if (data && typeof data === 'object') {
      return data[path];
    }

    return undef;
  }, locals);
};

export const resolveVariableAndFilter = (path, locals = {}, data = locals) => {
  if (!path) {
    return undef;
  }

  const m = /(\\)?\$(?:([a-z0-9_.]+|&)|{([^}]+?)})/i.exec(path);

  if (!m) {
    return undef;
  }

  const [_, escape, key, key2] = m;

  if (escape) {
    return _.substring(1);
  }

  let finalKey = key || key2;
  if (!~finalKey.indexOf('|')) {
    finalKey += '| raw';
  }

  let paths = finalKey.split(/\s*\|\s*/g);
  let originalKey = finalKey;
  finalKey = paths.shift();

  let ret = resolveVariable(finalKey, locals, data);

  return ret == null && !~originalKey.indexOf('default') ? '' : paths.reduce((input, filter) => {
      let params = filter.split(':');
      let key = params.shift();

      return (filters[key] || filters.raw).call(locals, input, ...params);
    }, ret);
};

export const setVariable = (path, value, data = {}) => {
  // if (path[0] === '$') {
  //   path = path.substring(1);
  // }

  if (!path) {
    return data;
  }

  let parts = path.split('.');
  let newData = parts.reduceRight((value, path) => {
    let data = {};
    if (path) {
      data[path] = value;
    }
    return data;
  }, value);

  return assign({}, data, newData);
};

const entityMap = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': '&quot;',
  "'": '&#39;',
  "/": '&#x2F;'
};
export const escapeHtml = string => String(string).replace(/[&<>"'\/]/g, function (s) {
  return entityMap[s];
});

export const filter = (...args) => {
  const str = args[0];

  if (typeof str !== 'string' || !~str.indexOf('$') && !isTpl(str)) {
    return args.length > 1 ? str : () => str;
  }

  return str ? isTpl(str) ? tpl2html(...args) : tokenize(...args) : null;
};

export const padArr = (arr, size = 4) => {
  const ret = [];
  const pool = arr.concat();
  let from = 0;

  while (pool.length) {
    let host = ret[from] || (ret[from] = []);

    if (host.length >= size) {
      from += 1;
      continue;
    }

    host.push(pool.shift());
  }

  return ret;
};

export const tplImports = assign({}, filters, {

  countDown(end) {
    if (!end) {
      return '--';
    }

    let date = new Date(parseInt(end, 10) * 1000);
    let now = Date.now();

    if (date < now) {
      return '已结束';
    }

    return Math.ceil((date - now) / (1000 * 60 * 60 * 24)) + '天';
  },

  formatDate(value, format = 'LLL', inputFormat = '') {
    return moment(value, inputFormat).format(format);
  },

  formatTimeStamp(value, format = 'LLL') {
    return moment(value, 'X').format(format);
  },

  formatNumber(value) {
    return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  },


  forEach
});

export const buildSchemaApi = (api) => {
  const isObject = typeof api === 'object';
  let url = isObject ? api.url : api;
  const idx = url.indexOf('?');

  let params = {};
  if (~idx) {
    params = qs.parse(url.substring(idx + 1));
    url = url.substring(0, idx);
  }

  params['_replace'] = 1;

  let strParams = qs.stringify(params, {arrayFormat: 'brackets', encode: false});
  url = url + '?' + strParams;

  return isObject ? assign({}, api, {url}) : url;
};

// 不能用 default
tplImports['defaultValue'] = tplImports['default'];
delete tplImports['default'];

export const setAdapter = Ajax.setAdapter;
export const setRequestAdapter = Ajax.setRequestAdapter;
export const setResponseAdapter = Ajax.setResponseAdapter;

const mapping = {
  'minute': 'm',
  'minutes': 'm',
  'min': 'm',
  'mins': 'm',
  'hour': 'h',
  'hours': 'h',
  'day': 'd',
  'days': 'd',
  'week': 'w',
  'weeks': 'w',
  'month': 'M',
  'months': 'M',
  'year': 'Y',
  'years': 'Y'
};

const relativeValueRe = /^(.+)?(\+|-)(\d+)(minute|minutes|min|mins|hours|hour|day|days|week|weeks|month|months|year|years|weekday)$/i;

export const filterDate = (value, data, format = 'X') => {
  let m;

  value = filter(value, data);

  if (value && typeof value === 'string'
    && (m = relativeValueRe.exec(value))) {
    const date = new Date();
    const step = parseInt(m[3]);
    const from = m[1] ? filterDate(m[1], data, format) : moment([date.getFullYear(), date.getMonth(), date.getDate()]);
    return from[m[2] === '-' ? 'subtract' : 'add'](step, mapping[m[4]] || m[4]);
  } else if (value === 'now') {
    return moment();
  } else if (value === 'today') {
    const date = new Date();
    return moment([date.getFullYear(), date.getMonth(), date.getDate()]);
  } else if (value) {
    return moment(value, format)
  }

  return value;
};

export const toBol = (value) => !!(+value || value === 'true');

export const isEmptyObject = obj => !obj || isEmpty(obj) || keys(obj).every(item => !obj[item]);

